package cfengMiddleware.server.service.impl;

import CfengMiddleWare.model.entity.UserAccount;
import CfengMiddleWare.model.entity.UserAccountRecord;
import CfengMiddleWare.model.mapper.UserAccountMapper;
import CfengMiddleWare.model.mapper.UserAccountRecordMapper;
import cfengMiddleware.server.dto.UserAccountDto;
import cfengMiddleware.server.service.UseAccountService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * @author Cfeng
 * @date 2022/9/20
 */

@RequiredArgsConstructor
@Service
@Slf4j
public class UseAccountServiceImpl implements UseAccountService {

    //数据库表连接对象
    private final UserAccountMapper userAccountMapper;

    private final UserAccountRecordMapper accountRecordMapper;

    private final RedisTemplate redisTemplate;

    //操作Zookeeper的实例
    private final CuratorFramework zookeeperClient;

    private final RedissonClient redissonClient;

    /**
     * 无所方式进行提现，用户发起请求后，首先查询账户，如果够提现就进行提现，提现成功后就可以进行异步打款，rabbitMQ
     */
    @Override
    public void takeMoney(UserAccountDto userAccountDto) throws Exception {
        UserAccount userAccount = userAccountMapper.selectByUserId(userAccountDto.getUserId());
        //校验
        if(!Objects.isNull(userAccount) && userAccount.getAmount().doubleValue() >= userAccountDto.getAccount()) {
            //提现
            int res = userAccountMapper.updateAmount(userAccountDto.getAccount(),userAccount.getId());
            //记录
            if(res > 0) {
                UserAccountRecord record = new UserAccountRecord();
                record.setCreateTime(LocalDateTime.now());
                record.setAccountId(userAccount.getId());
                record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));
                accountRecordMapper.insert(record);
                log.info("当前待提现金额为:{},账户的余额为： {}, 成功",userAccountDto.getAccount(),userAccount.getAmount());
            } else {
                throw new Exception("账户余额不足");
            }
        } else {
            throw new Exception("账户不存在或者余额不足!");
        }
    }

    //乐观锁方式，主要就是查询账户的时候会一起查询出来
    @Override
    public void takeMoneyWithVersion(UserAccountDto userAccountDto) throws Exception {
        //乐观锁方式会校验version
        UserAccount userAccount = userAccountMapper.selectByUserId(userAccountDto.getUserId());

        if(!Objects.isNull(userAccount) && userAccount.getAmount().doubleValue() >= userAccountDto.getAccount()) {
            //更新以最开始查询的version为准，如果发现已经被更新了就会更新失败,最先到达的更新成功
           int res =  userAccountMapper.updateByPKVersion(userAccountDto.getAccount(),userAccount.getId(),userAccount.getVersion());
            if(res > 0) {
                //第一个线程更新成功，写入记录
                UserAccountRecord record = new UserAccountRecord();
                record.setCreateTime(LocalDateTime.now());
                record.setAccountId(userAccount.getId());
                record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));
                accountRecordMapper.insert(record);
                log.info("乐观锁-当前待提现金额：{}, 账户总金额：{}",userAccountDto.getAccount(),userAccount.getAmount());
            } else {
                throw new Exception("乐观锁 - 账户余额不足");
            }
        } else {
            throw new Exception("乐观锁 - 账户不存在或者余额不足");
        }
    }

    @Override
    public void takeMoneyWithLock(UserAccountDto userAccountDto) throws Exception {
        //悲观锁在查询时加上for update，开始操作时加锁，直到操作结束事务释放
        UserAccount userAccount = userAccountMapper.selectByUserIdLock(userAccountDto.getUserId());

        if(userAccount != null && userAccount.getAmount().doubleValue() >= userAccountDto.getAccount()) {
            //提现
            int res = userAccountMapper.updateAmount(userAccountDto.getAccount(),userAccount.getId());
            //和之前的是相同的，现在IDEA都不提倡重复的代码，最好能够复用
            if(res > 0) {
                UserAccountRecord record = new UserAccountRecord();
                record.setCreateTime(LocalDateTime.now());
                record.setAccountId(userAccount.getId());
                record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));

                accountRecordMapper.insert(record);
                log.info("悲观锁 -当前待提现金额为:{},账户的余额为： {}, 成功",userAccountDto.getAccount(),userAccount.getAmount());
            } else {
                throw new Exception("悲观锁 - 账户余额不足");
            }
        } else {
            throw new Exception("悲观锁 - 账户不存在或者余额不足");
        }
    }

    //redis方式实现分布式锁主要就是依靠的SETNX和EXPIRE，具体到代码就是setIfAbsent，之前使用过，lock锁的名称最好与资源相关，而value可以生成时间戳nanoTime和UUID
    @Override
    public void takeMoneyWithRedisLock(UserAccountDto userAccountDto) throws Exception {
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //读取操作，修改操作之前需要获取lock锁，也就是redis的一个key
        //首先还是需要先进行读取，读取大家都可以，修改操作就是一个一个来
        UserAccount userAccount = userAccountMapper.selectByUserId(userAccountDto.getUserId());
        //之后进行判断
        if(!Objects.isNull(userAccount) && userAccount.getAmount().doubleValue() - userAccountDto.getAccount() >= 0) {
            //如果余额够，那么就进行提现，写操作，加分布式锁【redis的lock锁】,操作的是元组，所以主要是避免同一个用户的不同线程
            final String key = "redis:account:" + userAccountDto.getUserId() + "-lock";
            //value就是一个唯一确定的，所以UUID可以选择，加上纳秒级时间戳即可
            final String value = System.nanoTime() + "" + UUID.randomUUID();
            //调用SETNX获取锁
            boolean lock = valueOperations.setIfAbsent(key, value);
            //本身原子性
            if (lock) {
                //为了表面死锁，一般还需要设置Key的过期时间，这里就先20s,因为只是为了应对瞬时高并发
                redisTemplate.expire(key, 20, TimeUnit.SECONDS);
                //获取到锁后执行提现操作
               //同时为了保证不出现死锁，分布式锁方案（互斥、公平、不出现死锁、可重入、高可用），为了高可用、不死锁，使用finally确保锁释放
                try {
                    //提现
                    int res = userAccountMapper.updateAmount(userAccountDto.getAccount(), userAccount.getId());
                    //和之前的是相同的，现在IDEA都不提倡重复的代码，最好能够复用
                    if (res > 0) {
                        UserAccountRecord record = new UserAccountRecord();
                        record.setCreateTime(LocalDateTime.now());
                        record.setAccountId(userAccount.getId());
                        record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));

                        accountRecordMapper.insert(record);
                        log.info("redis分布式锁 -当前待提现金额为:{},账户的余额为： {}, 成功", userAccountDto.getAccount(), userAccount.getAmount());
                        //成功后结束
                        return;
                    } else {
                        throw new Exception("redis分布式锁 -- 余额不足");
                    }
                } catch (Exception e) {
                    throw e;
                } finally {
                    //不管发生什么异常情况，一定要释放锁，避免死锁，当然释放的是当前对象的锁，确保key和value对应
                    if(Objects.equals(value,valueOperations.get(key).toString())) {
                        redisTemplate.delete(key); //删除key，释放资源锁
                    }
                }
            }
        }
        throw new Exception("出现异常");
    }

    //基于zookeeper实现分布式锁
    @Override
    public void takeMoneyWithZookeeper(UserAccountDto userAccountDto) throws Exception {
        //主要依赖的是ZooKeeper的InterProcessMutex类进程互斥
        //Znode的节点路径
        final String pathPrefix = "/middleware/zkLock";
        //创建Zookeeper的进程互斥锁实例，尝试获取锁的最长的事件为20s,这里创建和资源相关的锁
        InterProcessMutex mutex = new InterProcessMutex(zookeeperClient,pathPrefix + userAccountDto.getUserId() + "-lock");
        try {
            //当前线程对应的节点尝试获取锁，获取的最大等待时间20s
            if(mutex.acquire(10L,TimeUnit.SECONDS)) {
                //当前线程获取到锁
                UserAccount userAccount = userAccountMapper.selectByUserId(userAccountDto.getUserId());
                if(!Objects.isNull(userAccount) && userAccount.getAmount().doubleValue() > userAccountDto.getAccount()) {
                    //提现
                    int res = userAccountMapper.updateAmount(userAccountDto.getAccount(),userAccount.getId());
                    if(res > 0) {
                        //提现成功
                        UserAccountRecord record = new UserAccountRecord();
                        record.setCreateTime(LocalDateTime.now());
                        record.setAccountId(userAccount.getId());
                        record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));

                        accountRecordMapper.insert(record);
                        log.info("zookeeper分布式锁 -当前待提现金额为:{},账户的余额为： {}, 成功", userAccountDto.getAccount(), userAccount.getAmount());
                        //成功后结束
                    } else {
                        throw new Exception("更新出现异常");
                    }
                } else {
                    throw new Exception("账户余额不足");
                }
            } else {
                throw new Exception("获取分布式锁失败Zookeeper");
            }
        } catch (Exception e) {
            throw e;
        } finally {
            //为了保证不死锁，不管正常还是异常，都需要释放锁
            mutex.release();
        }
    }

    /**
     * 基于Redisson 的lock方式，一直阻塞直到获取
     */
    @Override
    public void takeMoneyWithRedisson(UserAccountDto userAccountDto) throws Exception {
        final String key = "redisson:account:" + userAccountDto.getUserId() + "-lock";

        //获取分布式锁实例（可重入锁）
        RLock lock = redissonClient.getLock(key);

        //操作共享资源之前上锁，这里lock.lock方法会一直阻塞直到获取到锁
        try {
            //这里先设置锁的释放时间，10s后自动释放，看门狗可以进行监控
            lock.lock(10L,TimeUnit.SECONDS);
            //当前线程锁住资源
            UserAccount userAccount = userAccountMapper.selectByUserId(userAccountDto.getUserId());

            if(!Objects.isNull(userAccount) && userAccount.getAmount().doubleValue() > userAccountDto.getAccount()) {
                int res = userAccountMapper.updateAmount(userAccountDto.getAccount(),userAccount.getId());
                if(res > 0) {
                    //提现成功
                    UserAccountRecord record = new UserAccountRecord();
                    record.setCreateTime(LocalDateTime.now());
                    record.setAccountId(userAccount.getId());
                    record.setMoney(BigDecimal.valueOf(userAccountDto.getAccount()));

                    accountRecordMapper.insert(record);
                    log.info("Redisson分布式锁lock -当前待提现金额为:{},账户的余额为： {}, 成功", userAccountDto.getAccount(), userAccount.getAmount());
                    //成功后结束
                } else {
                    throw new Exception("更新出现异常");
                }
            }
        } catch (Exception e) {
            log.info("出现异常： {}",e.fillInStackTrace());
        } finally {
            if(lock != null) {
                //释放分布式锁，这里可以使用Lua保证锁是当前锁
                lock.forceUnlock();
            }
        }
    }

    /**
     * 基于redisson实现可重入锁
     */
    @Override
    public void takeMoneyWithRedissonReentrantLock(UserAccountDto userAccountDto) throws Exception {

    }
}
